Mocking AWS Errors without aws-sdk-mock

When testing unit behaviour in the event of an error, it can be helpful to mock when AWS returns some kind of error. The aws-sdk typically throws an AWSError object when it fails (as a promise rejection).

Unfortunately, this data type is not something jest will typically handle in its expect().toThrow() matching hook.

This pattern is known to work if there is a wrapper typescript service around the AWS SDK service (in the below example as /app/service/s3-service.ts ), but could potentially work with aws-sdk-mock as well.

We can use the following pattern to mock an error response. This sample uses S3.

// File: /app/service/s3-service.ts
import { S3 } from 'aws-sdk';

/**
 * Retrieves a file from an S3 Bucket as an S3 Object
 * @param Bucket The S3 bucket to fetch from
 * @param Key The name + path of the file to fetch
 * @returns The file as an S3 Object Promise
 */
export const getFile = async (Bucket: string, Key: string) =>
    new S3()
        .getObject({
            Bucket,
            Key,
        })
        .promise();


// File: /app/service/s3-wrapper-service.ts
import * as s3 from './s3-service';
import type { PromiseResult } from 'aws-sdk/lib/request';
import type { AWSError, S3 } from 'aws-sdk';

/**
 * Fetches the contents of a file from an S3 bucket as a string. Returns an empty string if the file doesn't exist. Throws an {@link AWSError} on any other failure.
 * @param bucketName The name of the S3 bucket to read from
 * @param key The file name + path to read.
 * @returns The body of the file as a string. If the file doesn't exist, returns an empty string. If there is another error, throws it.
 */
export async function fetchFromS3(bucketName: string, key: string): Promise<string> {
    let file: PromiseResult<S3.GetObjectOutput, AWSError>;

    try {
        file = await s3.getFile(bucketName, key);
    } catch (e) {
        const error = e as AWSError;

        // If no files exists - just treat it as an empty file
        if (error.code === 'NoSuchKey') {
            return '';
        }
        throw e;
    }
    if (!file.Body) {
        // If the file doesn't exist - just treat it as an empty one
        return '';
    }
    return file.Body.toString();
}
// File: /test/s3-wrapper-service.test.ts
import * as s3 from 'app/service/s3-service';
import { fetchFromS3 } from 'app/service/s3-wrapper-service';
import type { AWSError } from 'aws-sdk';

jest.mock('app/service/s3-service');

describe('fetchFromS3', () => {
    it('Returns an empty string if S3 returned `NoSuchKey` as an error', async () => {
        const noSuchKeyError: AWSError = {
            code: 'NoSuchKey',
            message: 'NoSuchKey error',
            name: 'No Such Key error or something',
            time: new Date(),
        };
        (s3.getFile as jest.Mock).mockRejectedValue(noSuchKeyError);
        const result = await fetchFromS3(process.env.DH_MIRROR_BUCKET!, 'testfile.csv');
        expect(result).toEqual('');
    });
    
    it("Throws S3 errors that aren't `NoSuchKey`", async () => {
      const otherError: AWSError = {
          code: 'ServiceUnavailable',
          message: 'need more kfc 21 piece buckets',
          name: 'ServiceUnavailable',
          time: new Date(),
      };
      (s3.getFile as jest.Mock).mockRejectedValue(otherError);
    
      try {
          await fetchFromS3('test-bucket', 'current.json');
          fail('Expected to throw but did not');
      } catch (e) {
          expect(s3.getFile).toBeCalledWith('test-bucket', 'current.json');
          expect(e).toBe(otherError);
      }
    });
});

Note the try/fail/catch block in the second it() statement.