diff --git a/apps/backend/src/applications/application.service.spec.ts b/apps/backend/src/applications/application.service.spec.ts index 3ea2a66b0..a58016fc4 100644 --- a/apps/backend/src/applications/application.service.spec.ts +++ b/apps/backend/src/applications/application.service.spec.ts @@ -73,6 +73,25 @@ describe('ApplicationsService', () => { expect(repository.find).toHaveBeenCalled(); expect(result).toEqual(mockApplications); }); + + it('should return an empty array if the repo returns one', async () => { + mockRepository.find.mockResolvedValue([]); + + const result = await service.findAll(); + + expect(repository.find).toHaveBeenCalled(); + expect(result).toEqual([]); + }); + + it('should pass along any repo errors without information loss', async () => { + mockRepository.find.mockRejectedValue( + new Error('There was a problem retrieving the info'), + ); + + await expect(service.findAll()).rejects.toThrow( + `There was a problem retrieving the info`, + ); + }); }); describe('findById', () => { @@ -115,6 +134,68 @@ describe('ApplicationsService', () => { where: { appId: nonExistentId }, }); }); + + // TODO: Address this in codebase so it passes. + // Note: Adding .skip for now so it doesn't confuse people in their develop then tests all pass work cycle + it.skip('should not return an application from the repo if the id is not the same as asked for', async () => { + const mockApplication: Application = { + appId: 1, + appStatus: AppStatus.APP_SUBMITTED, + daysAvailable: 'Monday, Tuesday', + experienceType: ExperienceType.BS, + fileUploads: [], + interest: InterestArea.NURSING, + license: null, + isInternational: false, + isLearner: false, + phone: '123-456-7890', + school: School.HARVARD_MEDICAL_SCHOOL, + referred: false, + referredEmail: null, + weeklyHours: 20, + }; + + mockRepository.findOne.mockResolvedValue(mockApplication); + + const result = await service.findById(10); + + expect(repository.findOne).toHaveBeenCalledWith({ where: { appId: 10 } }); + expect(repository.findOne).toThrow(); + }); + + it('should handle returning an application with no changes when optional fields are ommitted', async () => { + const mockApplication: Application = { + appId: 1, + appStatus: AppStatus.APP_SUBMITTED, + daysAvailable: 'Monday, Tuesday', + experienceType: ExperienceType.BS, + fileUploads: [], + interest: InterestArea.NURSING, + license: null, + isInternational: false, + isLearner: false, + phone: '123-456-7890', + school: School.HARVARD_MEDICAL_SCHOOL, + weeklyHours: 20, + }; + + mockRepository.findOne.mockResolvedValue(mockApplication); + + const result = await service.findById(1); + + expect(repository.findOne).toHaveBeenCalledWith({ where: { appId: 1 } }); + expect(result).toEqual(mockApplication); + }); + + it('should pass along any repo errors without information loss', async () => { + mockRepository.findOne.mockRejectedValue( + new Error('There was a problem retrieving the info'), + ); + + await expect(service.findById(1)).rejects.toThrow( + new Error(`There was a problem retrieving the info`), + ); + }); }); describe('create', () => { @@ -147,6 +228,197 @@ describe('ApplicationsService', () => { expect(repository.save).toHaveBeenCalled(); expect(result).toEqual(savedApplication); }); + + it('should pass along any repo errors without information loss', async () => { + mockRepository.save.mockRejectedValue( + new Error('There was a problem retrieving the info'), + ); + const mockApplication: CreateApplicationDto = { + appStatus: AppStatus.APP_SUBMITTED, + daysAvailable: 'Monday, Tuesday', + experienceType: ExperienceType.BS, + fileUploads: [], + interest: InterestArea.NURSING, + license: null, + isInternational: false, + isLearner: false, + phone: '123-456-7890', + school: School.HARVARD_MEDICAL_SCHOOL, + weeklyHours: 20, + }; + + await expect(service.create(mockApplication)).rejects.toThrow( + new Error(`There was a problem retrieving the info`), + ); + }); + + // TODO: Address this in codebase so it passes. + // Note: Adding .skip for now so it doesn't confuse people in their develop then tests all pass work cycle + it.skip('should not accept an invalid daysAvailable that is not in days of the week', async () => { + const createApplicationDto: CreateApplicationDto = { + appStatus: AppStatus.APP_SUBMITTED, + daysAvailable: 'Christmas, Thanksgiving', + experienceType: ExperienceType.BS, + fileUploads: [], + interest: InterestArea.NURSING, + license: null, + isInternational: false, + isLearner: false, + phone: '123-456-7890', + school: School.HARVARD_MEDICAL_SCHOOL, + referred: false, + referredEmail: null, + weeklyHours: 20, + }; + + const savedApplication: Application = { + appId: 1, + ...createApplicationDto, + }; + + mockRepository.save.mockResolvedValue(savedApplication); + await expect(service.create(createApplicationDto)).rejects.toThrow(); + }); + + // TODO: Address this in codebase so it passes. + // Note: Adding .skip for now so it doesn't confuse people in their develop then tests all pass work cycle + it.skip('should not accept a phone number that is too long', async () => { + const createApplicationDto: CreateApplicationDto = { + appStatus: AppStatus.APP_SUBMITTED, + daysAvailable: 'Monday, Tuesday', + experienceType: ExperienceType.BS, + fileUploads: [], + interest: InterestArea.NURSING, + license: null, + isInternational: false, + isLearner: false, + phone: '123-456-78901231', + school: School.HARVARD_MEDICAL_SCHOOL, + referred: false, + referredEmail: null, + weeklyHours: 20, + }; + + const savedApplication: Application = { + appId: 1, + ...createApplicationDto, + }; + + mockRepository.save.mockResolvedValue(savedApplication); + await expect(service.create(createApplicationDto)).rejects.toThrow(); + }); + + // TODO: Address this in codebase so it passes. + // Note: Adding .skip for now so it doesn't confuse people in their develop then tests all pass work cycle + it.skip('should not accept a phone number that is too short', async () => { + const createApplicationDto: CreateApplicationDto = { + appStatus: AppStatus.APP_SUBMITTED, + daysAvailable: 'Monday, Tuesday', + experienceType: ExperienceType.BS, + fileUploads: [], + interest: InterestArea.NURSING, + license: null, + isInternational: false, + isLearner: false, + phone: '123-4562', + school: School.HARVARD_MEDICAL_SCHOOL, + referred: false, + referredEmail: null, + weeklyHours: 20, + }; + + const savedApplication: Application = { + appId: 1, + ...createApplicationDto, + }; + + mockRepository.save.mockResolvedValue(savedApplication); + await expect(service.create(createApplicationDto)).rejects.toThrow(); + }); + + // TODO: Address this in codebase so it passes. + // Note: Adding .skip for now so it doesn't confuse people in their develop then tests all pass work cycle + it.skip('should not accept a phone number that is the right length but not in ###-###-#### format', async () => { + const createApplicationDto: CreateApplicationDto = { + appStatus: AppStatus.APP_SUBMITTED, + daysAvailable: 'Monday, Tuesday', + experienceType: ExperienceType.BS, + fileUploads: [], + interest: InterestArea.NURSING, + license: null, + isInternational: false, + isLearner: false, + phone: '123-456-8-90', + school: School.HARVARD_MEDICAL_SCHOOL, + referred: false, + referredEmail: null, + weeklyHours: 20, + }; + + const savedApplication: Application = { + appId: 1, + ...createApplicationDto, + }; + + mockRepository.save.mockResolvedValue(savedApplication); + await expect(service.create(createApplicationDto)).rejects.toThrow(); + }); + + // TODO: Address this in codebase so it passes. + // Note: Adding .skip for now so it doesn't confuse people in their develop then tests all pass work cycle + it.skip('should not accept 0 weekly hours', async () => { + const createApplicationDto: CreateApplicationDto = { + appStatus: AppStatus.APP_SUBMITTED, + daysAvailable: 'Monday, Tuesday', + experienceType: ExperienceType.BS, + fileUploads: [], + interest: InterestArea.NURSING, + license: null, + isInternational: false, + isLearner: false, + phone: '123-456-7890', + school: School.HARVARD_MEDICAL_SCHOOL, + referred: false, + referredEmail: null, + weeklyHours: 0, + }; + + const savedApplication: Application = { + appId: 1, + ...createApplicationDto, + }; + + mockRepository.save.mockResolvedValue(savedApplication); + await expect(service.create(createApplicationDto)).rejects.toThrow(); + }); + + // TODO: Address this in codebase so it passes. + // Note: Adding .skip for now so it doesn't confuse people in their develop then tests all pass work cycle + it.skip('should not accept negative weekly hours', async () => { + const createApplicationDto: CreateApplicationDto = { + appStatus: AppStatus.APP_SUBMITTED, + daysAvailable: 'Monday, Tuesday', + experienceType: ExperienceType.BS, + fileUploads: [], + interest: InterestArea.NURSING, + license: null, + isInternational: false, + isLearner: false, + phone: '123-456-78901231', + school: School.HARVARD_MEDICAL_SCHOOL, + referred: false, + referredEmail: null, + weeklyHours: -5, + }; + + const savedApplication: Application = { + appId: 1, + ...createApplicationDto, + }; + + mockRepository.save.mockResolvedValue(savedApplication); + await expect(service.create(createApplicationDto)).rejects.toThrow(); + }); }); describe('update', () => { @@ -242,6 +514,82 @@ describe('ApplicationsService', () => { }); expect(repository.save).not.toHaveBeenCalled(); }); + + it('should pass along any repo errors from retrieval without information loss when saving a new interest', async () => { + mockRepository.findOne.mockRejectedValue( + new Error('There was a problem retrieving the info'), + ); + + await expect( + service.update(1, { interest: InterestArea.HARM_REDUCTION }), + ).rejects.toThrow(new Error(`There was a problem retrieving the info`)); + }); + + it('should pass along any repo errors from retrieval without information loss when saving a new application status', async () => { + mockRepository.findOne.mockRejectedValue( + new Error('There was a problem retrieving the info'), + ); + + await expect( + service.update(1, { appStatus: AppStatus.IN_REVIEW }), + ).rejects.toThrow(new Error(`There was a problem retrieving the info`)); + }); + + it('should pass along any repo errors from saving the new info without information loss when saving a new interest', async () => { + const mockApplication: Application = { + appId: 1, + appStatus: AppStatus.APP_SUBMITTED, + daysAvailable: 'Monday, Tuesday', + experienceType: ExperienceType.BS, + fileUploads: [], + interest: InterestArea.NURSING, + license: null, + isInternational: false, + isLearner: false, + phone: '123-456-7890', + school: School.HARVARD_MEDICAL_SCHOOL, + referred: false, + referredEmail: null, + weeklyHours: 20, + }; + + mockRepository.findOne.mockResolvedValue(mockApplication); + mockRepository.save.mockRejectedValue( + new Error('There was a problem retrieving the info'), + ); + + await expect( + service.update(1, { interest: InterestArea.HARM_REDUCTION }), + ).rejects.toThrow(new Error(`There was a problem retrieving the info`)); + }); + + it('should pass along any repo errors from saving the new info without information loss when saving a new application status', async () => { + const mockApplication: Application = { + appId: 1, + appStatus: AppStatus.APP_SUBMITTED, + daysAvailable: 'Monday, Tuesday', + experienceType: ExperienceType.BS, + fileUploads: [], + interest: InterestArea.NURSING, + license: null, + isInternational: false, + isLearner: false, + phone: '123-456-7890', + school: School.HARVARD_MEDICAL_SCHOOL, + referred: false, + referredEmail: null, + weeklyHours: 20, + }; + + mockRepository.findOne.mockResolvedValue(mockApplication); + mockRepository.save.mockRejectedValue( + new Error('There was a problem retrieving the info'), + ); + + await expect( + service.update(1, { appStatus: AppStatus.IN_REVIEW }), + ).rejects.toThrow(new Error(`There was a problem retrieving the info`)); + }); }); describe('delete', () => { diff --git a/apps/backend/src/aws-s3/aws-s3.service.spec.ts b/apps/backend/src/aws-s3/aws-s3.service.spec.ts index e5e3adefc..4dab88ca1 100644 --- a/apps/backend/src/aws-s3/aws-s3.service.spec.ts +++ b/apps/backend/src/aws-s3/aws-s3.service.spec.ts @@ -1,9 +1,17 @@ import * as dotenv from 'dotenv'; import path from 'path'; -dotenv.config({ path: path.join(__dirname, '../../../.env') }); +dotenv.config({ path: path.join(__dirname, '../../../../.env') }); -import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3'; +import { + DeleteObjectsCommand, + S3Client, + S3ServiceException, + waitUntilObjectNotExists, + PutObjectCommand, + GetObjectCommand, + NoSuchKey, +} from '@aws-sdk/client-s3'; import { AWSS3Service } from './aws-s3.service'; import { mockClient } from 'aws-sdk-client-mock'; import axios from 'axios'; @@ -59,7 +67,7 @@ describe('AWSS3Service', () => { ).rejects.toThrow('File upload to AWS failed: Error: fail'); }); - // take off ".skip" to run this test but do so sparingly + // take off ".skip" to run this test. It does cleanup automatically but READ/WRITES can still pile up it.skip('should actually upload a file to S3 (integration)', async () => { s3Mock.restore(); const fileContent = `integration-test-content-${Date.now()}`; @@ -71,6 +79,7 @@ describe('AWSS3Service', () => { const url = await integrationService.upload(buffer, fileName, mimeType); console.log('Uploaded file URL:', url); expect(url).toContain(fileName); + try { const response = await axios.get(url); expect(response.status).toBe(200); @@ -79,7 +88,111 @@ describe('AWSS3Service', () => { throw new Error( `Failed to fetch the uploaded file from S3. Error: ${error.message}.`, ); + } finally { + // cleanup uploaded object(s) from S3 using helper + try { + await deleteObjects({ + bucketName: process.env.AWS_BUCKET_NAME, + keys: [fileName], + }); + } catch (cleanupErr) { + // don't mask test failure — log cleanup issues + // eslint-disable-next-line no-unsafe-finally + throw new Error( + 'Failed to clean up S3 objects after integration test:' + cleanupErr, + ); + } + + try { + await getObjectFromS3({ + bucketName: process.env.AWS_BUCKET_NAME, + key: fileName, + }); + } catch (err) { + if (err instanceof NoSuchKey) { + console.log( + 'object with filename ' + + fileName + + ' is successfully no longer in the bucket. No manual steps required', + ); + } else { + // eslint-disable-next-line no-unsafe-finally + throw new Error( + 'There was an error trying to check if the object is no longer in the bucket. Please check the bucket manually to ensure proper cleanup.', + ); + } + } } }, 15000); + // MAKE SURE TO CLEAN UP THE FILES FROM OUR S3 BUCKET AFTER RUNNING THE INTEGRATION TEST }); + +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. SPDX-License-Identifier: Apache-2.0 +// For the following code to the end of the file, which has been modified: +/** + * Delete multiple objects from an S3 bucket. + * @param {{ bucketName: string, keys: string[] }} + */ +const deleteObjects = async ({ bucketName, keys }) => { + const client = new S3Client({ + region: process.env.AWS_REGION, + credentials: { + accessKeyId: process.env.AWS_ACCESS_KEY_ID, + secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY, + }, + }); + + try { + const { Deleted } = await client.send( + new DeleteObjectsCommand({ + Bucket: bucketName, + Delete: { + Objects: keys.map((k) => ({ Key: k })), + }, + }), + ); + for (const k of keys) { + await waitUntilObjectNotExists( + { client, maxWaitTime: 30 }, + { Bucket: bucketName, Key: k }, + ); + } + console.log( + `Successfully deleted ${Deleted.length} objects from S3 bucket. Deleted objects:`, + ); + console.log(Deleted.map((d) => ` • ${d.Key}`).join('\n')); + } catch (caught) { + if ( + caught instanceof S3ServiceException && + caught.name === 'NoSuchBucket' + ) { + throw new Error( + `Error from S3 while deleting objects from ${bucketName}. The bucket doesn't exist.`, + ); + } else if (caught instanceof S3ServiceException) { + throw new Error( + `Error from S3 while deleting objects from ${bucketName}. ${caught.name}: ${caught.message}`, + ); + } else { + throw caught; + } + } +}; + +/** + * Get a single object from a specified S3 bucket. + * @param {{ bucketName: string, key: string }} + */ +const getObjectFromS3 = async ({ bucketName, key }) => { + const client = new S3Client({}); + const response = await client.send( + new GetObjectCommand({ + Bucket: bucketName, + Key: key, + }), + ); + // The Body object also has 'transformToByteArray' and 'transformToWebStream' methods. + const str = await response.Body.transformToString(); + console.log(str); +}; diff --git a/apps/backend/src/learners/learners.controller.spec.ts b/apps/backend/src/learners/learners.controller.spec.ts index fc6f97567..166a6cdcd 100644 --- a/apps/backend/src/learners/learners.controller.spec.ts +++ b/apps/backend/src/learners/learners.controller.spec.ts @@ -123,6 +123,18 @@ describe('LearnersController', () => { expect(result).toEqual([]); }); + + it('should error out without information loss if the service throws an error', async () => { + jest + .spyOn(mockLearnersService, 'findAll') + .mockRejectedValue( + new Error('There was a problem retrieving the info'), + ); + + await expect(controller.getAllLearners()).rejects.toThrow( + `There was a problem retrieving the info`, + ); + }); }); describe('getLearner', () => { diff --git a/apps/backend/src/learners/learners.service.spec.ts b/apps/backend/src/learners/learners.service.spec.ts index 63a7ba977..7380d26a7 100644 --- a/apps/backend/src/learners/learners.service.spec.ts +++ b/apps/backend/src/learners/learners.service.spec.ts @@ -94,6 +94,52 @@ describe('LearnersService', () => { ), ).rejects.toThrow('Start date must be before end date'); }); + + it('should error out without information loss if the repository throws an error during create', async () => { + const createData = { + appId: 1, + name: 'John Doe', + startDate: new Date('2024-01-01'), + endDate: new Date('2024-06-30'), + }; + + jest + .spyOn(mockLearnersRepository, 'create') + .mockImplementationOnce(() => { + throw new Error('There was a problem retrieving the info'); + }); + + await expect( + service.create( + createData.appId, + createData.name, + createData.startDate, + createData.endDate, + ), + ).rejects.toThrow(`There was a problem retrieving the info`); + }); + + it('should error out without information loss if the repository throws an error during save', async () => { + const createData = { + appId: 1, + name: 'John Doe', + startDate: new Date('2024-01-01'), + endDate: new Date('2024-06-30'), + }; + + jest.spyOn(mockLearnersRepository, 'save').mockImplementationOnce(() => { + throw new Error('There was a problem saving the info'); + }); + + await expect( + service.create( + createData.appId, + createData.name, + createData.startDate, + createData.endDate, + ), + ).rejects.toThrow(`There was a problem saving the info`); + }); }); describe('findOne', () => { @@ -122,6 +168,18 @@ describe('LearnersService', () => { 'Learner with ID 999 not found', ); }); + + it('should error out without information loss if the repository throws an error during retrieval', async () => { + jest + .spyOn(mockLearnersRepository, 'findOneBy') + .mockRejectedValueOnce( + new Error('There was a problem retrieving the info'), + ); + + await expect(service.findOne(1)).rejects.toThrow( + 'There was a problem retrieving the info', + ); + }); }); describe('findAll', () => { @@ -142,6 +200,18 @@ describe('LearnersService', () => { expect(result).toEqual([]); }); + + it('should error out without information loss if the repository throws an error during retrieval', async () => { + jest + .spyOn(mockLearnersRepository, 'find') + .mockRejectedValueOnce( + new Error('There was a problem retrieving the info'), + ); + + await expect(service.findAll()).rejects.toThrow( + 'There was a problem retrieving the info', + ); + }); }); describe('findByAppId', () => { @@ -170,6 +240,18 @@ describe('LearnersService', () => { 'Valid app ID is required', ); }); + + it('should error out without information loss if the repository throws an error during retrieval', async () => { + jest + .spyOn(mockLearnersRepository, 'find') + .mockRejectedValueOnce( + new Error('There was a problem retrieving the info'), + ); + + await expect(service.findByAppId(8)).rejects.toThrow( + 'There was a problem retrieving the info', + ); + }); }); describe('updateStartDate', () => { @@ -221,6 +303,33 @@ describe('LearnersService', () => { 'Start date is required', ); }); + + it('should error out without information loss if the repository throws an error during retrieval', async () => { + jest + .spyOn(mockLearnersRepository, 'findOneBy') + .mockRejectedValueOnce( + new Error('There was a problem retrieving the info'), + ); + + await expect( + service.updateStartDate(999, updatedStartDate), + ).rejects.toThrow('There was a problem retrieving the info'); + }); + + it('should error out without information loss if the repository throws an error during save', async () => { + jest + .spyOn(mockLearnersRepository, 'findOneBy') + .mockResolvedValue(learner1); + jest + .spyOn(mockLearnersRepository, 'save') + .mockRejectedValueOnce( + new Error('There was a problem saving the info'), + ); + + await expect( + service.updateStartDate(1, updatedStartDate), + ).rejects.toThrow('There was a problem saving the info'); + }); }); describe('updateEndDate', () => { @@ -272,5 +381,32 @@ describe('LearnersService', () => { 'End date is required', ); }); + + it('should error out without information loss if the repository throws an error during retrieval', async () => { + jest + .spyOn(mockLearnersRepository, 'findOneBy') + .mockRejectedValueOnce( + new Error('There was a problem retrieving the info'), + ); + + await expect(service.updateEndDate(999, updatedEndDate)).rejects.toThrow( + 'There was a problem retrieving the info', + ); + }); + + it('should error out without information loss if the repository throws an error during save', async () => { + jest + .spyOn(mockLearnersRepository, 'findOneBy') + .mockResolvedValue(learner1); + jest + .spyOn(mockLearnersRepository, 'save') + .mockRejectedValueOnce( + new Error('There was a problem saving the info'), + ); + + await expect(service.updateEndDate(1, updatedEndDate)).rejects.toThrow( + 'There was a problem saving the info', + ); + }); }); }); diff --git a/apps/backend/src/users/admins.service.spec.ts b/apps/backend/src/users/admins.service.spec.ts index 941f7b957..b58836168 100644 --- a/apps/backend/src/users/admins.service.spec.ts +++ b/apps/backend/src/users/admins.service.spec.ts @@ -76,12 +76,44 @@ describe('AdminsService', () => { }; mockRepository.create.mockReturnValue(mockAdmin); - mockRepository.save.mockRejectedValue(new Error('Database error')); + mockRepository.save.mockRejectedValueOnce(new Error('Database error')); await expect(service.create(createAdminDto)).rejects.toThrow( 'Database error', ); }); + it('should pass along any repo errors without information loss during create', async () => { + const createAdminDto: CreateAdminDto = { + name: 'John Doe', + email: 'john@example.com', + site: Site.FENWAY, + }; + + mockRepository.create.mockImplementationOnce(() => { + throw new Error('There was a problem creating the entry'); + }); + + await expect(service.create(createAdminDto)).rejects.toThrow( + 'There was a problem creating the entry', + ); + }); + + it('should pass along any repo errors without information loss during save', async () => { + const createAdminDto: CreateAdminDto = { + name: 'John Doe', + email: 'john@example.com', + site: Site.FENWAY, + }; + + mockRepository.create.mockReturnValue(mockAdmin); + mockRepository.save.mockRejectedValueOnce( + new Error('There was a problem saving the entry'), + ); + + await expect(service.create(createAdminDto)).rejects.toThrow( + 'There was a problem saving the entry', + ); + }); }); describe('findAll', () => { @@ -90,7 +122,7 @@ describe('AdminsService', () => { mockAdmin, { ...mockAdmin, id: 2, email: 'jane@example.com' }, ]; - mockRepository.find.mockResolvedValue(mockAdmins); + mockRepository.find.mockResolvedValueOnce(mockAdmins); const result = await service.findAll(); @@ -99,12 +131,22 @@ describe('AdminsService', () => { }); it('should return empty array when no admins exist', async () => { - mockRepository.find.mockResolvedValue([]); + mockRepository.find.mockResolvedValueOnce([]); const result = await service.findAll(); expect(result).toEqual([]); }); + + it('should pass along any repo errors without information loss during retrieval', async () => { + mockRepository.find.mockRejectedValueOnce( + new Error('There was a problem retrieving the entries'), + ); + + await expect(service.findAll()).rejects.toThrow( + 'There was a problem retrieving the entries', + ); + }); }); describe('findOne', () => { @@ -124,6 +166,16 @@ describe('AdminsService', () => { new NotFoundException('Admin with ID 999 not found'), ); }); + + it('should pass along any repo errors without information loss during retrieval', async () => { + mockRepository.findOne.mockRejectedValueOnce( + new Error('There was a problem retrieving the entry'), + ); + + await expect(service.findOne(1)).rejects.toThrow( + 'There was a problem retrieving the entry', + ); + }); }); describe('findByEmail', () => { @@ -145,6 +197,16 @@ describe('AdminsService', () => { expect(result).toBeNull(); }); + + it('should pass along any repo errors without information loss during retrieval', async () => { + mockRepository.findOne.mockImplementationOnce(() => { + throw new Error('There was a problem retrieving the entries'); + }); + + await expect(service.findByEmail('n')).rejects.toThrow( + 'There was a problem retrieving the entries', + ); + }); }); describe('findBySite', () => { @@ -167,6 +229,16 @@ describe('AdminsService', () => { expect(result).toEqual([]); }); + + it('should pass along error information from repo with no loss', async () => { + mockRepository.find.mockRejectedValueOnce( + new Error('error during retrieval'), + ); + + await expect(service.findBySite(Site.SITE_A)).rejects.toThrow( + 'error during retrieval', + ); + }); }); describe('updateEmail', () => { @@ -217,6 +289,33 @@ describe('AdminsService', () => { expect(result.email).toBe('valid@example.com'); }); + + it('should pass along any repo errors without information loss during retrieval', async () => { + const updateEmailDto: UpdateAdminEmailDto = { + email: 'valid@example.com', + }; + + mockRepository.findOne.mockRejectedValueOnce( + new Error('There was a problem retrieving the entry'), + ); + + await expect(service.updateEmail(1, updateEmailDto)).rejects.toThrow( + 'There was a problem retrieving the entry', + ); + }); + + it('should pass along any repo errors without information loss during saving', async () => { + const updateEmailDto: UpdateAdminEmailDto = { + email: 'valid@example.com', + }; + mockRepository.findOne.mockResolvedValue(mockAdmin); + mockRepository.save.mockRejectedValueOnce( + new Error('There was a problem saving the entry'), + ); + await expect(service.updateEmail(1, updateEmailDto)).rejects.toThrow( + 'There was a problem saving the entry', + ); + }); }); describe('remove', () => { @@ -237,6 +336,26 @@ describe('AdminsService', () => { new NotFoundException('Admin with ID 999 not found'), ); }); + + it('should pass along any repo errors without information loss during retrieval', async () => { + mockRepository.findOne.mockRejectedValueOnce( + new Error('There was a problem retrieving the entry'), + ); + + await expect(service.remove(1)).rejects.toThrow( + 'There was a problem retrieving the entry', + ); + }); + + it('should pass along any repo errors without information loss during saving', async () => { + mockRepository.findOne.mockResolvedValue(mockAdmin); + mockRepository.remove.mockRejectedValueOnce( + new Error('There was a problem saving the entry'), + ); + await expect(service.remove(1)).rejects.toThrow( + 'There was a problem saving the entry', + ); + }); }); describe('edge cases', () => { diff --git a/apps/backend/src/util/email/email.service.spec.ts b/apps/backend/src/util/email/email.service.spec.ts index 6745b953e..89046bb9c 100644 --- a/apps/backend/src/util/email/email.service.spec.ts +++ b/apps/backend/src/util/email/email.service.spec.ts @@ -34,4 +34,13 @@ describe('EmailService', () => { await service.queueEmail('recipient@email.com', 'Subject', '