Skip to content

Commit

Permalink
Merge pull request #160 from bcgov/feat/coms-unit-tests
Browse files Browse the repository at this point in the history
Improve unit test coverage to above 50%
  • Loading branch information
jujaga authored Apr 24, 2023
2 parents 6bd6b16 + 32dbf1e commit 20b6187
Show file tree
Hide file tree
Showing 10 changed files with 1,337 additions and 76 deletions.
61 changes: 30 additions & 31 deletions app/src/services/metadata.js
Original file line number Diff line number Diff line change
Expand Up @@ -66,37 +66,6 @@ const service = {
}
},

/**
* @function deleteOrphanedMetadata
* deletes Metadata records if they are no longer related to any versions
* @param {object} [etrx=undefined] An optional Objection Transaction object
* @returns {Promise<number>} The result of running the delete operation (number of rows deleted)
* @throws The error encountered upon db transaction failure
*/
// TODO: check if deleteing a version will prune orphan metadata records (sister table)
pruneOrphanedMetadata: async (etrx = undefined) => {
let trx;
try {
trx = etrx ? etrx : await Metadata.startTransaction();

const deletedMetadataIds = await Metadata.query(trx)
.allowGraph('versionMetadata')
.withGraphJoined('versionMetadata')
.select('metadata.id')
.whereNull('versionMetadata.metadataId');

const response = await Metadata.query(trx)
.delete()
.whereIn('id', deletedMetadataIds.map(({ id }) => id));

if (!etrx) await trx.commit();
return Promise.resolve(response);
} catch (err) {
if (!etrx && trx) await trx.rollback();
throw err;
}
},

/**
* @function createMetadata
* Inserts any metadata records if they dont already exist in db
Expand Down Expand Up @@ -232,6 +201,36 @@ const service = {
}));
},

/**
* @function pruneOrphanedMetadata
* deletes Metadata records if they are no longer related to any versions
* @param {object} [etrx=undefined] An optional Objection Transaction object
* @returns {Promise<number>} The result of running the delete operation (number of rows deleted)
* @throws The error encountered upon db transaction failure
*/
// TODO: check if deleting a version will prune orphan metadata records (sister table)
pruneOrphanedMetadata: async (etrx = undefined) => {
let trx;
try {
trx = etrx ? etrx : await Metadata.startTransaction();

const deletedMetadataIds = await Metadata.query(trx)
.allowGraph('versionMetadata')
.withGraphJoined('versionMetadata')
.select('metadata.id')
.whereNull('versionMetadata.metadataId');

const response = await Metadata.query(trx)
.delete()
.whereIn('id', deletedMetadataIds.map(({ id }) => id));

if (!etrx) await trx.commit();
return Promise.resolve(response);
} catch (err) {
if (!etrx && trx) await trx.rollback();
throw err;
}
},

/**
* @function searchMetadata
Expand Down
24 changes: 19 additions & 5 deletions app/tests/common/helper.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,15 +46,29 @@ const helper = {
},

/**
* @function resetReturnThis
* Updates all jest mocked attributes in `obj` to `mockReturnThis`
* @param {object} obj An object with some mocked attributes
* @function resetModel
* Resets a mock objection model
* @param {object} obj A mock objection model
* @param {object} trx A mock transaction object
*/
resetReturnThis: (obj) => {
resetModel: (obj, trx) => {
// Set all jest functions to return itself
Object.keys(obj).forEach((f) => {
if (jest.isMockFunction(obj[f])) obj[f].mockReturnThis();
});
}
obj.startTransaction.mockImplementation(() => trx);
obj.then.mockImplementation((resolve) => resolve(this));
},

/**
* @function trxBuilder
* Returns a mock transaction object
* @returns {object} A mock transaction object
*/
trxBuilder: () => ({
commit: jest.fn(),
rollback: jest.fn()
})
};

module.exports = helper;
170 changes: 170 additions & 0 deletions app/tests/unit/services/bucket.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
const { NIL: BUCKET_ID, NIL: SYSTEM_USER } = require('uuid');

const { resetModel, trxBuilder } = require('../../common/helper');
const Bucket = require('../../../src/db/models/tables/bucket');

const bucketTrx = trxBuilder();
jest.mock('../../../src/db/models/tables/bucket', () => ({
startTransaction: jest.fn(),
then: jest.fn(),

allowGraph: jest.fn(),
deleteById: jest.fn(),
findById: jest.fn(),
first: jest.fn(),
insert: jest.fn(),
modify: jest.fn(),
patchAndFetchById: jest.fn(),
query: jest.fn(),
returning: jest.fn(),
throwIfNotFound: jest.fn(),
where: jest.fn()
}));

const service = require('../../../src/services/bucket');
const bucketPermissionService = require('../../../src/services/bucketPermission');

const data = {
bucketId: BUCKET_ID,
bucketName: 'bucketName',
accessKeyId: 'accesskeyid',
bucket: 'bucket',
endpoint: 'endpoint',
key: 'key',
secretAccessKey: 'secretaccesskey',
region: 'region',
active: 'true',
createdBy: SYSTEM_USER,
userId: SYSTEM_USER
};

beforeEach(() => {
jest.clearAllMocks();
resetModel(Bucket, bucketTrx);
});

describe('checkGrantPermissions', () => {
const readUniqueSpy = jest.spyOn(service, 'readUnique');

beforeEach(() => {
readUniqueSpy.mockReset();
});

afterAll(() => {
readUniqueSpy.mockRestore();
});

it('Grants a user full permissions to the bucket if the data precisely matches', async () => {
readUniqueSpy.mockResolvedValue({ accessKeyId: data.accessKeyId, secretAccessKey: data.secretAccessKey });

await service.checkGrantPermissions(data);

expect(Bucket.startTransaction).toHaveBeenCalledTimes(1);
expect(bucketTrx.commit).toHaveBeenCalledTimes(1);
});
});

describe('create', () => {
const addPermissionsSpy = jest.spyOn(bucketPermissionService, 'addPermissions');

beforeEach(() => {
addPermissionsSpy.mockReset();
});

afterAll(() => {
addPermissionsSpy.mockRestore();
});

it('Create a bucket record and give the uploader (if authed) permissions', async () => {
addPermissionsSpy.mockResolvedValue({ ...data });

await service.create(data);

expect(Bucket.startTransaction).toHaveBeenCalledTimes(1);
expect(Bucket.query).toHaveBeenCalledTimes(1);
expect(Bucket.insert).toHaveBeenCalledTimes(1);
expect(Bucket.insert).toBeCalledWith(expect.anything());
expect(Bucket.returning).toHaveBeenCalledTimes(1);
expect(Bucket.returning).toBeCalledWith('*');
expect(bucketTrx.commit).toHaveBeenCalledTimes(1);
});
});

describe('delete', () => {
it('Delete a bucket record, this will also delete all objects and permissions', async () => {
await service.delete(BUCKET_ID);

expect(Bucket.startTransaction).toHaveBeenCalledTimes(1);
expect(Bucket.query).toHaveBeenCalledTimes(1);
expect(Bucket.deleteById).toHaveBeenCalledTimes(1);
expect(Bucket.deleteById).toBeCalledWith(BUCKET_ID);
expect(Bucket.throwIfNotFound).toHaveBeenCalledTimes(1);
expect(Bucket.throwIfNotFound).toBeCalledWith();
expect(Bucket.returning).toHaveBeenCalledTimes(1);
expect(Bucket.returning).toBeCalledWith('*');
expect(bucketTrx.commit).toHaveBeenCalledTimes(1);
});
});

describe('searchBuckets', () => {
it('search and filter for specific bucket records', async () => {
Bucket.then.mockImplementation(() => { });

await service.searchBuckets([]);

expect(Bucket.query).toHaveBeenCalledTimes(1);
expect(Bucket.allowGraph).toHaveBeenCalledTimes(1);
expect(Bucket.modify).toHaveBeenCalledTimes(5);
expect(Bucket.then).toHaveBeenCalledTimes(1);
});
});

describe('read', () => {
it('Get a bucket db record based on bucketId', async () => {
await service.read(BUCKET_ID);

expect(Bucket.query).toHaveBeenCalledTimes(1);
expect(Bucket.findById).toHaveBeenCalledTimes(1);
expect(Bucket.findById).toBeCalledWith(BUCKET_ID);
expect(Bucket.throwIfNotFound).toHaveBeenCalledTimes(1);
expect(Bucket.throwIfNotFound).toBeCalledWith();
});
});

describe('readUnique', () => {
it('Get a bucket db record based on unique parameters', async () => {
await service.readUnique(data);

expect(Bucket.query).toHaveBeenCalledTimes(1);
expect(Bucket.where).toHaveBeenCalledTimes(3);
expect(Bucket.where).toBeCalledWith('bucket', expect.any(String));
expect(Bucket.where).toBeCalledWith('endpoint', expect.any(String));
expect(Bucket.where).toBeCalledWith('key', expect.any(String));
expect(Bucket.first).toHaveBeenCalledTimes(1);
expect(Bucket.first).toBeCalledWith();
expect(Bucket.throwIfNotFound).toHaveBeenCalledTimes(1);
expect(Bucket.throwIfNotFound).toBeCalledWith();
});
});

describe('update', () => {
it('Update a bucket DB record', async () => {
await service.update(data);

expect(Bucket.startTransaction).toHaveBeenCalledTimes(1);
expect(Bucket.query).toHaveBeenCalledTimes(1);
expect(Bucket.patchAndFetchById).toHaveBeenCalledTimes(1);
expect(Bucket.patchAndFetchById).toBeCalledWith(data.bucketId, {
bucketName: data.bucketName,
accessKeyId: data.accessKeyId,
bucket: data.bucket,
endpoint: data.endpoint,
key: data.key,
secretAccessKey: data.secretAccessKey,
region: data.region,
active: data.active,
updatedBy: data.userId
});
expect(bucketTrx.commit).toHaveBeenCalledTimes(1);
});
});
116 changes: 116 additions & 0 deletions app/tests/unit/services/bucketPermission.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
const { NIL: BUCKET_ID, NIL: OBJECT_ID, NIL: SYSTEM_USER } = require('uuid');

const { resetModel, trxBuilder } = require('../../common/helper');
const BucketPermission = require('../../../src/db/models/tables/bucketPermission');
const ObjectPermission = require('../../../src/db/models/tables/objectPermission');

const bucketPermissionTrx = trxBuilder();
jest.mock('../../../src/db/models/tables/bucketPermission', () => ({
startTransaction: jest.fn(),
then: jest.fn(),

delete: jest.fn(),
insertAndFetch: jest.fn(),
modify: jest.fn(),
query: jest.fn(),
returning: jest.fn()
}));

const objectPermissionTrx = trxBuilder();
jest.mock('../../../src/db/models/tables/objectPermission', () => ({
startTransaction: jest.fn(),
then: jest.fn(),

distinct: jest.fn(),
joinRelated: jest.fn(),
modify: jest.fn(),
query: jest.fn(),
select: jest.fn(),
whereNotNull: jest.fn()
}));

const service = require('../../../src/services/bucketPermission');

const data = [{
id: OBJECT_ID,
bucketId: BUCKET_ID,
path: 'path',
public: 'true',
active: 'true',
createdBy: SYSTEM_USER,
permCode: 'READ'
}];

beforeEach(() => {
jest.clearAllMocks();
resetModel(BucketPermission, bucketPermissionTrx);
resetModel(ObjectPermission, objectPermissionTrx);
});

describe('addPermissions', () => {
const searchPermissionsSpy = jest.spyOn(service, 'searchPermissions');

beforeEach(() => {
searchPermissionsSpy.mockReset();
});

afterAll(() => {
searchPermissionsSpy.mockRestore();
});

it('Grants bucket permissions to users', async () => {
searchPermissionsSpy.mockResolvedValue([{ userId: SYSTEM_USER, permCode: 'READ' }]);

await service.addPermissions(BUCKET_ID, data);

expect(BucketPermission.startTransaction).toHaveBeenCalledTimes(1);
expect(BucketPermission.insertAndFetch).toHaveBeenCalledTimes(1);
expect(BucketPermission.insertAndFetch).toBeCalledWith(expect.anything());
expect(bucketPermissionTrx.commit).toHaveBeenCalledTimes(1);
});
});

describe('removePermissions', () => {
it('Deletes bucket permissions for a user', async () => {
await service.removePermissions(BUCKET_ID, [SYSTEM_USER]);

expect(BucketPermission.startTransaction).toHaveBeenCalledTimes(1);
expect(BucketPermission.delete).toHaveBeenCalledTimes(1);
expect(BucketPermission.delete).toBeCalledWith();
expect(BucketPermission.modify).toHaveBeenCalledTimes(3);
expect(BucketPermission.modify).toBeCalledWith('filterUserId', [SYSTEM_USER]);
expect(BucketPermission.modify).toBeCalledWith('filterBucketId', BUCKET_ID);
expect(BucketPermission.returning).toHaveBeenCalledTimes(1);
expect(BucketPermission.returning).toBeCalledWith('*');
expect(bucketPermissionTrx.commit).toHaveBeenCalledTimes(1);
});
});

describe('getBucketIdsWithObject', () => {
it('Searches for specific (bucket) object permissions', async () => {
ObjectPermission.then.mockImplementation();

await service.getBucketIdsWithObject();

expect(ObjectPermission.query).toHaveBeenCalledTimes(1);
expect(ObjectPermission.select).toHaveBeenCalledTimes(1);
expect(ObjectPermission.distinct).toHaveBeenCalledTimes(1);
expect(ObjectPermission.joinRelated).toHaveBeenCalledTimes(1);
expect(ObjectPermission.modify).toHaveBeenCalledTimes(1);
expect(ObjectPermission.whereNotNull).toHaveBeenCalledTimes(1);
expect(ObjectPermission.then).toHaveBeenCalledTimes(1);
});
});

describe('searchPermissions', () => {
it('Search and filter for specific bucket permissions', () => {
service.searchPermissions({ userId: SYSTEM_USER, bucketId: BUCKET_ID, permCode: 'READ' });

expect(BucketPermission.query).toHaveBeenCalledTimes(1);
expect(BucketPermission.modify).toHaveBeenCalledTimes(3);
expect(BucketPermission.modify).toBeCalledWith('filterUserId', SYSTEM_USER);
expect(BucketPermission.modify).toBeCalledWith('filterBucketId', BUCKET_ID);
expect(BucketPermission.modify).toBeCalledWith('filterPermissionCode', 'READ');
expect(BucketPermission.modify).toHaveBeenCalledTimes(3);
});
});
Loading

0 comments on commit 20b6187

Please sign in to comment.