Skip to content

Commit

Permalink
increased coverage, fixed logic error in office-sync
Browse files Browse the repository at this point in the history
Jira ticket: CAMS-442

Co-authored-by: Arthur Morrow <[email protected]>
Co-authored-by: James Brooks <[email protected]>
Co-authored-by: Brian Posey <[email protected]>,
  • Loading branch information
3 people committed Nov 8, 2024
1 parent 7c4a977 commit 0ce6dcb
Show file tree
Hide file tree
Showing 4 changed files with 131 additions and 56 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -57,13 +57,12 @@ describe('offices repo', () => {
...session.user,
ttl,
});

const insertOneSpy = jest
.spyOn(MongoCollectionAdapter.prototype, 'insertOne')
const replaceOneSpy = jest
.spyOn(MongoCollectionAdapter.prototype, 'replaceOne')
.mockResolvedValue('inserted-id');

await repo.putOfficeStaff(officeCode, session.user);
expect(insertOneSpy).toHaveBeenCalledWith({ ...staff, updatedOn: expect.anything() });
expect(replaceOneSpy).toHaveBeenCalledWith(expect.anything(), staff, true);
});

describe('error handling', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,11 @@ export class OfficesMongoRepository extends BaseMongoRepository implements Offic
...user,
ttl,
});
const query = QueryBuilder.build(
and(equals<string>('id', staff.id), equals<string>('officeCode', officeCode)),
);
try {
await this.getAdapter<OfficeStaff>().insertOne(staff);
await this.getAdapter<OfficeStaff>().replaceOne(query, staff, true);
} catch (originalError) {
throw getCamsError(originalError, MODULE_NAME);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { CamsError } from '../../../../common-errors/cams-error';
import { NotFoundError } from '../../../../common-errors/not-found-error';
import { UnknownError } from '../../../../common-errors/unknown-error';
import { CollectionHumble } from '../../../../humble-objects/mongo-humble';
import { CollectionHumble, DocumentClient } from '../../../../humble-objects/mongo-humble';
import QueryBuilder from '../../../../query/query-builder';
import { MongoCollectionAdapter } from './mongo-adapter';
import { MongoCollectionAdapter, removeIds } from './mongo-adapter';

const { and, orderBy } = QueryBuilder;

Expand All @@ -29,7 +29,11 @@ const spies = {
countDocuments,
};

type TestType = object;
type TestType = {
_id?: unknown;
id?: string;
foo?: string;
};

describe('Mongo adapter', () => {
const testQuery = QueryBuilder.build(and());
Expand All @@ -40,13 +44,37 @@ describe('Mongo adapter', () => {
jest.resetAllMocks();
});

test('should return an instance of the adapter from newAdapter', () => {
const mockClient: DocumentClient = {
database: () => {
return {
collection: <_t>() => {},
};
},
} as unknown as DocumentClient;
const adapter = MongoCollectionAdapter.newAdapter<TestType>(
'module',
'collection',
'database',
mockClient,
);
expect(adapter).toBeInstanceOf(MongoCollectionAdapter);
});

test('should return items from a getAll', async () => {
find.mockResolvedValue([{}, {}, {}]);
const item = await adapter.getAll();
expect(item).toEqual([{}, {}, {}]);
expect(find).toHaveBeenCalled();
});

// TODO: maybe remove this?
test('should remove Ids from an item', async () => {
const expectedItem = { arbitraryValue: 'arbitrary-value' };
const item = { _id: 'some_id', id: 'someId', ...expectedItem };
expect(removeIds(item)).toEqual(expectedItem);
});

test('should return a sorted list of items from a getAll', async () => {
function* generator() {
yield Promise.resolve({});
Expand Down Expand Up @@ -101,24 +129,77 @@ describe('Mongo adapter', () => {
);
});

test('should return a single Id from a replaceOne', async () => {
const id = '123456';
replaceOne.mockResolvedValue({ acknowdledged: true, upsertedId: id });
const result = await adapter.replaceOne(testQuery, {});
expect(result).toEqual(id);
test('should return a single Id from replaceOne', async () => {
const testObject: TestType = { id: '12345', foo: 'bar' };
const _id = 'mongoGeneratedId';
replaceOne.mockResolvedValue({
acknowledged: true,
matchedCount: 1,
modifiedCount: 1,
upsertedId: _id,
});
const result = await adapter.replaceOne(testQuery, testObject);
expect(result).not.toEqual(_id);
expect(result).toEqual(testObject.id);
});

test('should throw an error calling replaceOne for a nonexistant record and upsert=false', async () => {
const testObject: TestType = { id: '12345', foo: 'bar' };
replaceOne.mockResolvedValue({
acknowledged: false,
matchedCount: 0,
modifiedCount: 0,
upsertedId: null,
});
await expect(adapter.replaceOne(testQuery, testObject)).rejects.toThrow(
'No matching item found.',
);
});

test('should return a single Id from replaceOne when upsert = true', async () => {
const testObject: TestType = { id: '12345', foo: 'bar' };
const _id = 'mongoGeneratedId';

replaceOne.mockResolvedValue({
acknowledged: true,
matchedCount: 0,
modifiedCount: 1,
upsertedId: _id,
});
const result = await adapter.replaceOne(testQuery, testObject, true);
expect(result).toEqual(testObject.id);
expect(result).not.toEqual(_id);
});

test('should throw an error if replaceOne does not match.', async () => {
const testObject: TestType = { id: '12345', foo: 'bar' };
replaceOne.mockResolvedValue({
acknowledged: false,
matchedCount: 0,
modifiedCount: 0,
upsertedId: null,
});
await expect(adapter.replaceOne(testQuery, testObject, true)).rejects.toThrow(
'Failed to insert document into database.',
);
});

test('should return a single Id from insertOne', async () => {
const id = '123456';
insertOne.mockResolvedValue({ acknowdledged: true, insertedId: id });
insertOne.mockResolvedValue({ acknowledged: true, insertedId: id });
const result = await adapter.insertOne({});
expect(result.split('-').length).toEqual(5);
});

test('should throw an error if insertOne does not insert.', async () => {
insertOne.mockResolvedValue({ acknowledged: false });
await expect(adapter.insertOne({})).rejects.toThrow('Failed to insert document into database.');
});

test('should return a list of Ids from insertMany', async () => {
const ids = ['0', '1', '2', '3', '4'];
insertMany.mockResolvedValue({
acknowdledged: true,
acknowledged: true,
insertedIds: ids,
insertedCount: ids.length,
});
Expand All @@ -127,7 +208,7 @@ describe('Mongo adapter', () => {
});

test('should return a count of 1 for 1 item deleted', async () => {
deleteOne.mockResolvedValue({ acknowdledged: true, deletedCount: 1 });
deleteOne.mockResolvedValue({ acknowledged: true, deletedCount: 1 });
const result = await adapter.deleteOne(testQuery);
expect(result).toEqual(1);
});
Expand All @@ -140,7 +221,7 @@ describe('Mongo adapter', () => {
});

test('should return a count of 5 for 5 items deleted', async () => {
deleteMany.mockResolvedValue({ acknowdledged: true, deletedCount: 5 });
deleteMany.mockResolvedValue({ acknowledged: true, deletedCount: 5 });
const result = await adapter.deleteMany(testQuery);
expect(result).toEqual(5);
});
Expand All @@ -158,8 +239,15 @@ describe('Mongo adapter', () => {
expect(result).toEqual(5);
});

test('should return a count of 6 when countAllDocuments is called and there are 6 documents', async () => {
countDocuments.mockResolvedValue(6);
const result = await adapter.countAllDocuments();
expect(result).toEqual(6);
});

test('should throw CamsError when some but not all items are inserted', async () => {
insertMany.mockResolvedValue({
acknowledged: true,
insertedIds: {
one: 'one',
two: 'two',
Expand All @@ -176,22 +264,6 @@ describe('Mongo adapter', () => {
expect(async () => await adapter.insertMany([{}, {}, {}, {}])).rejects.toThrow(error);
});

test('should handle acknowledged == false', async () => {
const response = { acknowledged: false };
const error = new UnknownError(MODULE_NAME, {
message: 'Operation returned Not Acknowledged.',
});
Object.values(spies).forEach((spy) => {
spy.mockResolvedValue(response);
});

await expect(adapter.replaceOne(testQuery, {})).rejects.toThrow(error);
await expect(adapter.insertOne({})).rejects.toThrow(error);
await expect(adapter.insertMany([{}])).rejects.toThrow(error);
await expect(adapter.deleteOne(testQuery)).rejects.toThrow(error);
await expect(adapter.deleteMany(testQuery)).rejects.toThrow(error);
});

test('should handle errors', async () => {
const originalError = new Error('Test Exception');
const expectedError = new UnknownError(MODULE_NAME, { originalError });
Expand All @@ -206,6 +278,8 @@ describe('Mongo adapter', () => {
await expect(adapter.deleteMany(testQuery)).rejects.toThrow(expectedError);
await expect(adapter.find(testQuery)).rejects.toThrow(expectedError);
await expect(adapter.countDocuments(testQuery)).rejects.toThrow(expectedError);
await expect(adapter.countAllDocuments()).rejects.toThrow(expectedError);
await expect(adapter.findOne(testQuery)).rejects.toThrow(expectedError);
await expect(adapter.getAll()).rejects.toThrow(expectedError);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -10,21 +10,11 @@ import { randomUUID } from 'crypto';

export class MongoCollectionAdapter<T> implements DocumentCollectionAdapter<T> {
private collectionHumble: CollectionHumble<T>;
private readonly notAcknowledged: UnknownError;
private readonly moduleName: string;

private testAcknowledged(result: { acknowledged?: boolean }) {
if (result.acknowledged === false) {
throw this.notAcknowledged;
}
}

constructor(moduleName: string, collection: CollectionHumble<T>) {
this.collectionHumble = collection;
this.moduleName = moduleName;
this.notAcknowledged = new UnknownError(this.moduleName, {
message: 'Operation returned Not Acknowledged.',
});
}

public async find(query: ConditionOrConjunction, sort?: Sort): Promise<T[]> {
Expand Down Expand Up @@ -79,9 +69,16 @@ export class MongoCollectionAdapter<T> implements DocumentCollectionAdapter<T> {
const mongoItem = createOrGetId<T>(item);
try {
const result = await this.collectionHumble.replaceOne(mongoQuery, mongoItem, upsert);
this.testAcknowledged(result);

return result.upsertedId?.toString();
if (!result.acknowledged) {
if (upsert) {
throw new UnknownError(this.moduleName, {
message: 'Failed to insert document into database.',
});
} else {
throw new NotFoundError(this.moduleName, { message: 'No matching item found.' });
}
}
return mongoItem.id;
} catch (originalError) {
throw getCamsError(originalError, this.moduleName);
}
Expand All @@ -90,11 +87,15 @@ export class MongoCollectionAdapter<T> implements DocumentCollectionAdapter<T> {
public async insertOne(item: T) {
try {
const cleanItem = removeIds(item);
const identifiableItem = createOrGetId<T>(cleanItem);
const result = await this.collectionHumble.insertOne(identifiableItem);
this.testAcknowledged(result);
const mongoItem = createOrGetId<T>(cleanItem);
const result = await this.collectionHumble.insertOne(mongoItem);
if (!result.acknowledged) {
throw new UnknownError(this.moduleName, {
message: 'Failed to insert document into database.',
});
}

return identifiableItem.id;
return mongoItem.id;
} catch (originalError) {
throw getCamsError(originalError, this.moduleName);
}
Expand All @@ -107,7 +108,6 @@ export class MongoCollectionAdapter<T> implements DocumentCollectionAdapter<T> {
return createOrGetId<T>(cleanItem);
});
const result = await this.collectionHumble.insertMany(mongoItems);
this.testAcknowledged(result);
const insertedIds = mongoItems.map((item) => item.id);
if (insertedIds.length !== result.insertedCount) {
throw new CamsError(this.moduleName, {
Expand All @@ -125,7 +125,6 @@ export class MongoCollectionAdapter<T> implements DocumentCollectionAdapter<T> {
const mongoQuery = toMongoQuery(query);
try {
const result = await this.collectionHumble.deleteOne(mongoQuery);
this.testAcknowledged(result);
if (result.deletedCount !== 1) {
throw new NotFoundError(this.moduleName, { message: 'No items deleted' });
}
Expand All @@ -140,7 +139,6 @@ export class MongoCollectionAdapter<T> implements DocumentCollectionAdapter<T> {
const mongoQuery = toMongoQuery(query);
try {
const result = await this.collectionHumble.deleteMany(mongoQuery);
this.testAcknowledged(result);
if (result.deletedCount < 1) {
throw new NotFoundError(this.moduleName, { message: 'No items deleted' });
}
Expand Down Expand Up @@ -190,10 +188,11 @@ function createOrGetId<T>(item: CamsItem<T>): CamsItem<T> {
return mongoItem;
}

function removeIds<T>(item: CamsItem<T>): CamsItem<T> {
// TODO: sus.
export function removeIds<T>(item: CamsItem<T>): CamsItem<T> {
const cleanItem = { ...item };
delete cleanItem?._id;
delete cleanItem?.id;
delete cleanItem._id;
delete cleanItem.id;
return cleanItem;
}

Expand Down

0 comments on commit 0ce6dcb

Please sign in to comment.